s3 sdk
์๋ง์กด์ AWS S3 SDK๋ฅผ ์ฌ์ฉํด์ R2 ์๋น์ค๋ฅผ ์ด์ฉํ๋ค. ๊ทธ๋์ https://developers.cloudflare.com/r2/examples/aws/aws-sdk-java/ ๊ณต์ ๋ฌธ์์์ ์ ์ํ ์ฝ๋๋ฅผ ์ดํด๋ณด์๋ค.
์ฝ๋1 (๊ณต์)
/**
* Client for interacting with Cloudflare R2 Storage using AWS SDK S3 compatibility
*/
public class CloudflareR2Client {
private final S3Client s3Client;
/**
* Creates a new CloudflareR2Client with the provided configuration
*/
public CloudflareR2Client(S3Config config) {
this.s3Client = buildS3Client(config);
}
/**
* Configuration class for R2 credentials and endpoint
*/
public static class S3Config {
private final String accountId;
private final String accessKey;
private final String secretKey;
private final String endpoint;
public S3Config(String accountId, String accessKey, String secretKey) {
this.accountId = accountId;
this.accessKey = accessKey;
this.secretKey = secretKey;
this.endpoint = String.format(
"https://%s.r2.cloudflarestorage.com", accountId);
}
public String getAccessKey() { return accessKey; }
public String getSecretKey() { return secretKey; }
public String getEndpoint() { return endpoint; }
}
/**
* Builds and configures the S3 client with R2-specific settings
*/
private static S3Client buildS3Client(S3Config config) {
AwsBasicCredentials credentials = AwsBasicCredentials.create(
config.getAccessKey(),
config.getSecretKey()
);
S3Configuration serviceConfiguration = S3Configuration.builder()
.pathStyleAccessEnabled(true)
.build();
return S3Client.builder()
.endpointOverride(URI.create(config.getEndpoint()))
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.region(Region.of("auto"))
.serviceConfiguration(serviceConfiguration)
.build();
}
/**
* Lists all buckets in the R2 storage
*/
public List<Bucket> listBuckets() {
try {
return s3Client.listBuckets().buckets();
} catch (S3Exception e) {
throw new RuntimeException(
"Failed to list buckets: " + e.getMessage(), e);
}
}
/**
* Lists all objects in the specified bucket
*/
public List<S3Object> listObjects(String bucketName) {
try {
ListObjectsV2Request request = ListObjectsV2Request.builder()
.bucket(bucketName)
.build();
return s3Client.listObjectsV2(request).contents();
} catch (S3Exception e) {
throw new RuntimeException(
"Failed to list objects in bucket "
+ bucketName + ": " + e.getMessage(), e);
}
}
public static void main(String[] args) {
S3Config config = new S3Config(
"your_account_id",
"your_access_key",
"your_secret_key"
);
CloudflareR2Client r2Client = new CloudflareR2Client(config);
// List buckets
System.out.println("Available buckets:");
r2Client.listBuckets().forEach(bucket ->
System.out.println("* " + bucket.name())
);
// List objects in a specific bucket
String bucketName = "demos";
System.out.println("\nObjects in bucket '" + bucketName + "':");
r2Client.listObjects(bucketName).forEach(object ->
System.out.printf("* %s (size: %d bytes, modified: %s)%n",
object.key(),
object.size(),
object.lastModified())
);
}
}
์ด๊ฑด ์ด์ ์์ Java๋ฅผ ์ํ ์ฝ๋์ด๊ณ ์ด๊ฑธ ์คํ๋ง ํ๋ ์์ํฌ์ ๋ง์ถฐ์ ์์ฑํ๋ฉด
์ฝ๋2 (์คํ๋ง)
@Component
public class CloudflareR2Client {
private final S3Client s3Client;
public CloudflareR2Client(S3Client s3Client){
this.s3Client = s3Client;
}
public List<Bucket> listBuckets() {
try {
return s3Client.listBuckets().buckets();
} catch (S3Exception e) {
throw new RuntimeException(
"Failed to list buckets: " + e.getMessage(), e);
}
}
public List<S3Object> listObjects(String bucketName) {
try {
ListObjectsV2Request request = ListObjectsV2Request.builder()
.bucket(bucketName)
.build();
return s3Client.listObjectsV2(request).contents();
} catch (S3Exception e) {
throw new RuntimeException(
"Failed to list objects in bucket "
+ bucketName + ": " + e.getMessage(), e);
}
}
}
@Configuration
public class S3Config {
@Value("${cloudflare.r2.account.id}")
private String accountId;
@Value("${cloudflare.r2.access.key}")
private String accessKey;
@Value("${cloudflare.r2.secret.key}")
private String secretKey;
@Bean
public S3Client buildS3Client(){
String endpoint = String.format(
"https://%s.r2.cloudflarestorage.com", accountId);
AwsBasicCredentials credentials = AwsBasicCredentials.create(
accessKey, secretKey
);
S3Configuration serviceConfiguration = S3Configuration.builder()
.pathStyleAccessEnabled(true)
.build();
return S3Client.builder()
.endpointOverride(URI.create(endpoint))
.credentialsProvider(
StaticCredentialsProvider.create(credentials))
.region(Region.of("auto"))
.serviceConfiguration(serviceConfiguration)
.build();
}
}
์ฌ์ค S3Client
๋ฅผ ๊ทธ๋๋ก ์ฌ์ฉํ๋๊ฑด๋ฐ ์ ์ด๋ ๊ฒ ํ๋๊ฑด์ง๋ ๋ชจ๋ฅด๊ฒ ๋ค.
ํ์ด๋ ํ๋ก์ ํธ์์ ์ผ๋ s3config ์ฝ๋๋ ๊ฑฐ์ ํก์ฌ๋คํ๋ค.
@Configuration
public class S3Config {
@Value("${cloud.aws.credentials.access-key}")
private String accessKey;
@Value("${cloud.aws.credentials.secret-key}")
private String secretKey;
@Value("${cloud.aws.region.static}")
private String region;
@Bean
public AmazonS3Client amazonS3Client() {
// AWS ์ธ์ฆ ์ ๋ณด (Access Key, Secret Key) ์ค์
BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
return (AmazonS3Client) AmazonS3ClientBuilder
.standard() // ๊ธฐ๋ณธ์ค์
.withRegion(region)
.withCredentials(
new AWSStaticCredentialsProvider(credentials))
.build();
}
}
๋ฌธ์ ์
๋ํผ ๋ฉ์๋๋ฅผ ๊ณ์ ์ฌ์ฉํด์ผํ๋ค.
public void uploadImage(String bucket, String key, byte[] data){
try{
PutObjectRequest request = PutObjectRequest.builder()
.bucket(bucket)
.key(key)
.contentType("images")
.build();
s3Client.putObject(request, RequestBody.fromBytes(data));
} catch (S3Exception e){
throw new RuntimeException("Failed to upload image"+e.getMessage());
}
}
์ด๋ ๊ฒ ๋ฒ์ผ๊ณผ ์ฐ๊ด๋๋ ๊ธฐ๋ฅ์ ๋ค ์ ์ํด์ผํ๋ค.
์ ์ด์ s3client
๋ง ์ฌ์ฉํ๋ค๋ฉด service์ธต์์ ์ฆ์ ์ฌ์ฉํ ์ ์๊ฒ ์ง๋ง.
์๋ฌ
image service
์๋ฌ๋ฌธ
1
ava.lang.RuntimeException: Failed to upload imageThe request signature we calculated does not match the signature you provided. Check your secret access key and signing method. (Service: S3, Status Code: 403, Request ID: null) (SDK Attempt Count: 1)
2
=== Upload Debug Info ===
Bucket: temp
Key: 2505241824089439733_1.png
Data length: 663524
=== Error Details ===
Status Code: 403
Error Code: SignatureDoesNotMatch
Error Message: The request signature we calculated does not match the signature you provided. Check your secret access key and signing method.
Request ID: null
์ค๋ช
1
public void uploadImage(String bucket, String key, byte[] data){
System.out.println("=== Upload Debug Info ===");
System.out.println("Bucket: " + bucket);
System.out.println("Key: " + key);
System.out.println("Data length: " + data.length);
try{
PutObjectRequest request = PutObjectRequest.builder()
.bucket(bucket)
.key(key)
.build();
s3Client.putObject(request, RequestBody.fromBytes(data));
} catch (S3Exception e){
System.out.println("=== Error Details ===");
System.out.println("Status Code: " + e.statusCode());
System.out.println("Error Code: " + e.awsErrorDetails().errorCode());
System.out.println(
"Error Message: " + e.awsErrorDetails().errorMessage()
);
System.out.println("Request ID: " + e.requestId());
throw new RuntimeException("Failed to upload image"+e.getMessage());
}
}
์ฌ๊ธฐ์ S3Exception์ด ๋ฐ์ํ๋๊ฒ.
๊ทธ๋์ api (account) ํ ํฐ๋ ๋์ธ๋ฒ ๋ค์ ๋ฐ์๋ดค๋๋ฐ๋ ํด๊ฒฐ์ด ์๋๋ค. ๋ถ๋ช ํ ๊ถํ๋ admin read & write๋ก ์คฌ๋๋ฐ.
System.out.println(r2Client.listBuckets());
๋ฅผ ํ๋ฉด ๋ฒํท๋ค ๋ชฉ๋ก์ด ์ ๋ฑ์ฅํ๋๋ฐ. ์ ์
๋ก๋ ํ ๋๋ง ์๋์ง?
ํด๋ก๋๊ฐ ์ํค๋๋๋ก PreSigned URL์ ์ฌ์ฉํ๋ ๋ฐฉ์์ผ๋ก ์งํํด๋ดค๋ค. ๊ณต์๋ฌธ์์์๋
2
public class CloudflareR2Client {
private final S3Client s3Client;
private final S3Presigner presigner;
/**
* Creates a new CloudflareR2Client with the provided configuration
*/
public CloudflareR2Client(S3Config config) {
this.s3Client = buildS3Client(config);
this.presigner = buildS3Presigner(config);
}
/**
* Builds and configures the S3 presigner with R2-specific settings
*/
private static S3Presigner buildS3Presigner(S3Config config) {
AwsBasicCredentials credentials = AwsBasicCredentials.create(
config.getAccessKey(),
config.getSecretKey()
);
return S3Presigner.builder()
.endpointOverride(URI.create(config.getEndpoint()))
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.region(Region.of("auto"))
.serviceConfiguration(S3Configuration.builder()
.pathStyleAccessEnabled(true)
.build())
.build();
}
public String generatePresignedUploadUrl(
String bucketName,
String objectKey,
Duration expiration) {
PutObjectPresignRequest presignRequest =
PutObjectPresignRequest.builder()
.signatureDuration(expiration)
.putObjectRequest(builder -> builder
.bucket(bucketName)
.key(objectKey)
.build())
.build();
PresignedPutObjectRequest presignedRequest =
presigner.presignPutObject(presignRequest);
return presignedRequest.url().toString();
}
// Rest of the methods remains the same
public static void main(String[] args) {
// config the client as before
// Generate a pre-signed upload URL valid for 15 minutes
String uploadUrl = r2Client.generatePresignedUploadUrl(
"demos",
"README.md",
Duration.ofMinutes(15)
);
System.out.println("Pre-signed Upload URL (valid for 15 minutes):");
System.out.println(uploadUrl);
}
}
๊ทธ๋์ ๋๋
@Configuration
public class S3Config {
@Value("${cloudflare.r2.account.id}")
private String accountId;
@Value("${cloudflare.r2.access.key}")
private String accessKey;
@Value("${cloudflare.r2.secret.key}")
private String secretKey;
@Bean
public S3Client buildS3Client(){
...
}
@Bean
public S3Presigner s3Presigner() {
String endpoint = String.format("https://%s.r2.cloudflarestorage.com", accountId);
return S3Presigner.builder()
.endpointOverride(URI.create(endpoint))
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create(accessKey, secretKey)))
.region(Region.of("auto"))
.build();
}
}
์ด๋ ๊ฒ ๋ฐ์ S3Presigner
๋ง ์ถ๊ฐํด์คฌ๋ค.
๊ทธ๋ฆฌ๊ณ r2Client
ํด๋์ค์ ๋ฉ์๋๋ฅผ ์ถ๊ฐํ๋ค.
public String generateUploadUrl(String bucket, String key) {
try {
PutObjectRequest request = PutObjectRequest.builder()
.bucket(bucket)
.key(key)
.build();
PutObjectPresignRequest presignRequest =
PutObjectPresignRequest.builder()
.signatureDuration(Duration.ofMinutes(10))
.putObjectRequest(request)
.build();
String url =
s3Presigner.presignPutObject(presignRequest).url().toString();
System.out.println("Generated PreSigned URL: " + url);
return url;
} catch (Exception e) {
throw new RuntimeException(
"Failed to generate presigned URL: " + e.getMessage(), e);
}
}
๊ทธ๋ฆฌ๊ณ ๋์
String presignedUrl = r2Client.generateUploadUrl("temp", "presigned-test.txt");
System.out.println("Use this URL to upload: " + presignedUrl);
์ด๊ฑธ ํด๋ดค๋๋
Generated PreSigned URL: [https://temp.9af1ff692e57c2b144b127914c6d7c09.r2.cloudflarestorage.com/presigned-test.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20250524T095303Z&X-Amz-SignedHeaders=host&X-Amz-Credential=c4a6bd075832047b69d9c267867bcbe5%2F20250524%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Expires=600&X-Amz-Signature=2c3d37e7c2094eedb509f344763e314828b529e79737f00ad4e79a806f4a5f4d](https://temp.9af1ff692e57c2b144b127914c6d7c09.r2.cloudflarestorage.com/presigned-test.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20250524T095303Z&X-Amz-SignedHeaders=host&X-Amz-Credential=c4a6bd075832047b69d9c267867bcbe5%2F20250524%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Expires=600&X-Amz-Signature=2c3d37e7c2094eedb509f344763e314828b529e79737f00ad4e79a806f4a5f4d) Use this URL to upload: [https://temp.9af1ff692e57c2b144b127914c6d7c09.r2.cloudflarestorage.com/presigned-test.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20250524T095303Z&X-Amz-SignedHeaders=host&X-Amz-Credential=c4a6bd075832047b69d9c267867bcbe5%2F20250524%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Expires=600&X-Amz-Signature=2c3d37e7c2094eedb509f344763e314828b529e79737f00ad4e79a806f4a5f4d](https://temp.9af1ff692e57c2b144b127914c6d7c09.r2.cloudflarestorage.com/presigned-test.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20250524T095303Z&X-Amz-SignedHeaders=host&X-Amz-Credential=c4a6bd075832047b69d9c267867bcbe5%2F20250524%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Expires=600&X-Amz-Signature=2c3d37e7c2094eedb509f344763e314828b529e79737f00ad4e79a806f4a5f4d)
์ด๋ ๊ฒ ๊ธด ์ฃผ์๊ฐ ๋ฐํ๋๋ค.
๊ทธ๋ฆฌ๊ณ ๋ฒํท์ ํ์ธํด๋ณด๋ presigned-test.txt
๊ฐ ์ฑ๊ณต์ ์ผ๋ก ๋ง๋ค์ด์ก๋ค.
ํฐ๋ฏธ๋์ ์ผ์
curl -X "<์์์ฃผ์>" --data "Hello World Test"
๋ฅผ ํด๋ณด๋ ๋น์ด์๋ ํ
์คํธํ์ผ์ ํฌ๋ก์๋ ๋ฌธ๊ตฌ๊ฐ ์ ํ๋ค.
3
๊ทธ๋ฌ๋๊น presigned url์ ํ์ฉํ๋๊น ์ ๋๋ก ์๋์ ํ๊ฑด๋ฐ ์ค๊น?
ํด๋ก๋๋ S3Config
์ builder ๋ฉ์๋์ ์๋ serviceConfig
์ .chunkedEncodingEnabled(false)
๋ฅผ ์ถ๊ฐํด๋ณด๋ผ๊ณ ํ๋ค.
@Bean
public S3Client buildS3Client(){
String endpoint = String.format(
"https://%s.r2.cloudflarestorage.com", accountId);
AwsBasicCredentials credentials = AwsBasicCredentials.create(
accessKey, secretKey
);
S3Configuration serviceConfig = S3Configuration.builder()
.pathStyleAccessEnabled(true)
.chunkedEncodingEnabled(false) // ์ถ๊ฐ
.build();
return S3Client.builder()
.endpointOverride(URI.create(endpoint))
.credentialsProvider(
StaticCredentialsProvider.create(credentials))
.region(Region.of("auto"))
.serviceConfiguration(serviceConfig)
.build();
}
๊ทธ๋ฌ๋๋ ์ด์ ์ฌ์ง์ด ์ฌ๋ผ๊ฐ๋ค.
์ ๋ฆฌ
r2Client.listBuckets()
๊ฐ ์ ์๋ํ๋ค -> Credentials๊ฐ ์๋ชป ์ ๋ ฅ๋๊ฑด ์๋๋ผ๋๊ฒ์ด๋ค.- PreSigned url์ ์์ฑํ ์ ์์๊ณ
curl
๋ช ๋ น์ด๋ก ์ฌ๊ธฐ์ ์ ๋ก๋๋ ํ ์ ์์๋ค.
๊ทธ๋ฐ๋ฐ SDK๋ฅผ ํตํด์ putObject()
๋ง ๊ณ์ ์คํจํ๋ค?
S3 ํธํ ์๋น์ค๋ค์ AWS SDK๋ก ์ด์ฉํ ๋ ํํ ๋ฐ์ํ๋ ๋ฌธ์ ์ ๋ค
- region ์ค์ ์ฐจ์ด
- path style vs virtual hosted style
- chunked transfer encoding ํธํ์ฑ
- HTTP client ์ค์ ์ฐจ์ด